﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services.Client;
    using System.Data.Services.Common;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;

    /// <summary>Stores mapping between the server and client metadata</summary>
    public class MetadataMapping
    {
        /// <summary>The namespace for the client metadata.</summary>
        private string namespaceName;

        /// <summary>Dictionary of resource types. The key is the CLR instance type (which is the code-gened client type).</summary>
        private Dictionary<Type, ResourceType> resourceTypes;

        /// <summary>Dictionary of resource sets. The key is the name of the resource set.</summary>
        private Dictionary<string, ResourceSetMapping> resourceSets;

        /// <summary>The queue of resource types to be processed still.</summary>
        private Queue<ResourceTypeMapping> resourceTypeMappingsToProcess;

        /// <summary>Create new mapping from existing client context (code gened).</summary>
        /// <param name="namespaceName">The namespace name for the client metadata.</param>
        /// <param name="context">The context to infer metadata from.</param>
        public MetadataMapping(string namespaceName, DataServiceContext context)
            : this(namespaceName)
        {
            this.MapResourceSetsOnContextType(context.GetType());
        }

        /// <summary>Creates new mapping.</summary>
        /// <param name="namespaceName">The namespace name for the client metadata.</param>
        public MetadataMapping(string namespaceName)
        {
            this.namespaceName = namespaceName;
            this.resourceTypes = new Dictionary<Type, ResourceType>();
            this.resourceSets = new Dictionary<string, ResourceSetMapping>();
            this.resourceTypeMappingsToProcess = new Queue<ResourceTypeMapping>();
        }

        #region internal properties
        /// <summary>The namespace of the client metadata.</summary>
        internal string Namespace
        {
            get { return this.namespaceName; }
        }

        /// <summary>All client resource sets.</summary>
        internal IEnumerable<ResourceSetMapping> ResourceSets
        {
            get { return this.resourceSets.Values; }
        }

        /// <summary>All client resource types.</summary>
        internal IEnumerable<ResourceTypeMapping> ResourceTypes
        {
            get { return this.resourceTypes.Values.Where(rt => rt.ResourceTypeKind != ResourceTypeKind.Primitive).Cast<ResourceTypeMapping>(); }
        }
        #endregion

        /// <summary>Creates a new resource set mapping.</summary>
        /// <param name="resourceSetName">The name of the resource set on the server which will be used as client resource set name as well.</param>
        /// <param name="baseClientClrType">The base client CLR type for this resource set.</param>
        /// <remarks>The method will automatically map the <see cref="baseClientClrType"/> and all its derived types
        /// based on reflection.</remarks>
        public void MapResourceSet(string resourceSetName, Type baseClientClrType)
        {
            ResourceTypeMapping resourceTypeMapping = this.CreateEntityResourceTypeMapping(baseClientClrType);

            ResourceSetMapping resourceSetMapping = new ResourceSetMapping(resourceSetName, resourceTypeMapping);
            this.resourceSets.Add(resourceSetName, resourceSetMapping);
        }

        /// <summary>Determines resource set by its name.</summary>
        /// <param name="resourceSetName">The name of the resource set to find.</param>
        /// <param name="resourceSetMapping">The resource set found (or null if not found).</param>
        /// <returns>true if the resource set was found.</returns>
        internal bool TryGetResourceSet(string resourceSetName, out ResourceSetMapping resourceSetMapping)
        {
            return this.resourceSets.TryGetValue(resourceSetName, out resourceSetMapping);
        }

        /// <summary>Determines resource type by its name.</summary>
        /// <param name="resourceTypeName">The name of the resource type to find.</param>
        /// <param name="resourceTypeMapping">The resource type found (or null if not found).</param>
        /// <returns>true if the resource type was found.</returns>
        internal bool TryGetResourceType(string resourceTypeName, out ResourceTypeMapping resourceTypeMapping)
        {
            resourceTypeMapping = this.resourceTypes.Values.FirstOrDefault(rt => rt.FullName == resourceTypeName) as ResourceTypeMapping;
            return resourceTypeMapping != null;
        }

        /// <summary>Determines resource type from the CLR instance type.</summary>
        /// <param name="clientClrType">The CLR instance type to find the resource type for.</param>
        /// <param name="resourceTypeMapping">The resource type found (or null if not found).</param>
        /// <returns>true if the resource type was found.</returns>
        internal bool TryGetResourceType(Type clientClrType, out ResourceTypeMapping resourceTypeMapping)
        {
            resourceTypeMapping = null;
            ResourceType resourceType;
            if (this.resourceTypes.TryGetValue(clientClrType, out resourceType))
            {
                resourceTypeMapping = resourceType as ResourceTypeMapping;
            }

            return resourceTypeMapping != null;
        }

        /// <summary>Determines a resource association set.</summary>
        /// <param name="resourceSet">The resource set from which the association goes.</param>
        /// <param name="resourceType">The type of the source entity.</param>
        /// <param name="resourceProperty">The property which represents the association.</param>
        /// <returns><see cref="ResourceAssociationSet"/> which represents the specified relationship.</returns>
        internal ResourceAssociationSet GetResourceAssociationSet(
            ResourceSet resourceSet,
            ResourceType resourceType,
            ResourceProperty resourceProperty)
        {
            ResourceType targetResourceType = resourceProperty.ResourceType;
            ResourceSet targetResourceSet = this.ResourceSets.
                Where(rs => rs.ResourceType.InstanceType.IsAssignableFrom(targetResourceType.InstanceType)).Single();
            string associationName = resourceType.Name + '_' + resourceProperty.Name;
            ResourceAssociationSetEnd sourceEnd = new ResourceAssociationSetEnd(resourceSet, resourceType, resourceProperty);
            ResourceAssociationSetEnd targetEnd = new ResourceAssociationSetEnd(targetResourceSet, targetResourceType, null);
            return new ResourceAssociationSet(associationName, sourceEnd, targetEnd);
        }

        /// <summary>Initializes the metadata mapping.</summary>
        internal void InitializeMetadataMapping()
        {
            ResourceType resourceType;
            this.PopulateEnqueuedResourceTypeProperties();

            HashSet<Assembly> assemblies = new HashSet<Assembly>(EqualityComparer<Assembly>.Default);
            List<ResourceType> knownTypes = this.resourceTypes.Values.Where(rt => rt.BaseType == null).ToList();
            List<Type> derivedTypes = new List<Type>();
            foreach (ResourceType baseType in knownTypes)
            {
                Assembly assembly = baseType.InstanceType.Assembly;
                if (assemblies.Contains(assembly))
                {
                    continue;
                }

                assemblies.Add(assembly);

                foreach (Type type in assembly.GetTypes())
                {
                    if (!type.IsVisible)
                    {
                        continue;
                    }

                    if (knownTypes.Any(rt => rt.InstanceType == type))
                    {
                        continue;
                    }

                    if (knownTypes.Any(rt => rt.InstanceType.IsAssignableFrom(type)))
                    {
                        derivedTypes.Add(type);
                    }
                }
            }

            foreach (Type derivedType in derivedTypes)
            {
                Stack<Type> ancestorsAndSelf = new Stack<Type>();
                Type ancestorType = derivedType;
                ResourceType baseResourceType = null;
                while (ancestorType != null)
                {
                    if (this.resourceTypes.TryGetValue(ancestorType, out baseResourceType))
                    {
                        break;
                    }

                    ancestorsAndSelf.Push(ancestorType);
                    ancestorType = ancestorType.BaseType;
                }

                if (baseResourceType != null)
                {
                    while (ancestorsAndSelf.Count > 0 && (ancestorType = ancestorsAndSelf.Pop()) != null)
                    {
                        if (knownTypes.Any(rt => rt.InstanceType == ancestorType))
                        {
                            continue;
                        }

                        if (!this.resourceTypes.TryGetValue(ancestorType, out resourceType))
                        {
                            resourceType = this.CreateEntityResourceTypeMapping(ancestorType);
                        }

                        knownTypes.Add(resourceType);
                    }
                }
            }

            this.PopulateEnqueuedResourceTypeProperties();

            this.MarkMetadataAsReadonly();
        }

        /// <summary>Determines if the specified property is a Key property.</summary>
        /// <param name="property">The property to inspect.</param>
        /// <returns>true if the <paramref name="property"/> is a key property.</returns>
        private static bool IsPropertyKeyProperty(PropertyInfo property)
        {
            DataServiceKeyAttribute keyAttribute = property.ReflectedType.GetCustomAttributes(true).OfType<DataServiceKeyAttribute>().FirstOrDefault();
            if (keyAttribute != null && keyAttribute.KeyNames.Contains(property.Name))
            {
                return true;
            }
            else if (property.Name == property.DeclaringType.Name + "ID")
            {
                return true;
            }
            else if (property.Name == "ID")
            {
                return true;
            }

            return false;
        }

        /// <summary>Marks all the generated metadata as read only.</summary>
        private void MarkMetadataAsReadonly()
        {
            foreach (var rt in this.ResourceTypes)
            {
                rt.SetReadOnly();
            }

            foreach (var rs in this.ResourceSets)
            {
                rs.SetReadOnly();
            }
        }

        /// <summary>Discovers all IQueryable public properties on the specified context type and maps them as resource sets.</summary>
        /// <param name="contextType">The context type to examine.</param>
        private void MapResourceSetsOnContextType(Type contextType)
        {
            foreach (var property in contextType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (!property.CanRead)
                {
                    continue;
                }

                Type queryable = TypeSystem.FindIQueryable(property.PropertyType);
                if (queryable != null)
                {
                    this.MapResourceSet(property.Name, TypeSystem.GetIEnumerableElementType(queryable));
                }
            }
        }

        /// <summary>Populates properties for all types queued.</summary>
        private void PopulateEnqueuedResourceTypeProperties()
        {
            while (this.resourceTypeMappingsToProcess.Count > 0)
            {
                this.PopulateResourceTypeProperties(this.resourceTypeMappingsToProcess.Dequeue());
            }
        }

        /// <summary>Populates properties for specified resource type.</summary>
        /// <param name="resourceTypeMapping">The resource type to populate.</param>
        private void PopulateResourceTypeProperties(ResourceTypeMapping resourceTypeMapping)
        {
            Type clientClrType = resourceTypeMapping.InstanceType;

            BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
            if (resourceTypeMapping.BaseType != null)
            {
                bindingFlags |= BindingFlags.DeclaredOnly;
            }

            foreach (PropertyInfo propertyInfo in clientClrType.GetProperties(bindingFlags))
            {
                ResourcePropertyKind propertyKind = 0;

                Type propertyType = propertyInfo.PropertyType;
                Type enumerableElementType = TypeSystem.GetIEnumerableElementType(propertyType);
                if (enumerableElementType != null)
                {
                    propertyType = enumerableElementType;
                }

                ResourceType propertyResourceType;
                if (!this.resourceTypes.TryGetValue(propertyType, out propertyResourceType))
                {
                    propertyResourceType = this.CreatePrimitiveOrComplexResourceTypeMapping(propertyInfo.PropertyType);
                    if (propertyResourceType.ResourceTypeKind == ResourceTypeKind.ComplexType)
                    {
                        propertyKind = ResourcePropertyKind.ComplexType;
                    }
                    else
                    {
                        propertyKind = ResourcePropertyKind.Primitive;
                    }
                }
                else
                {
                    if (enumerableElementType != null)
                    {
                        propertyKind = ResourcePropertyKind.ResourceSetReference;
                        if (propertyResourceType.ResourceTypeKind != ResourceTypeKind.EntityType)
                        {
                            throw new NotSupportedException("Collections are only supported for entity types.");
                        }
                    }
                    else
                    {
                        propertyKind = ResourcePropertyKind.ResourceReference;
                    }
                }

                if (IsPropertyKeyProperty(propertyInfo))
                {
                    propertyKind |= ResourcePropertyKind.Key;
                }

                ResourceProperty property = new ResourceProperty(propertyInfo.Name, propertyKind, propertyResourceType);
                property.CanReflectOnInstanceTypeProperty = false;
                resourceTypeMapping.AddProperty(property);
            }
        }

        /// <summary>Creates a resource type mapping for the specified CLR type which is an entity type.</summary>
        /// <param name="clrType">The CLR instance client type.</param>
        /// <returns>The newly creates resource type.</returns>
        private ResourceTypeMapping CreateEntityResourceTypeMapping(Type clrType)
        {
            ResourceType baseResourceType = null;
            this.resourceTypes.TryGetValue(clrType.BaseType, out baseResourceType);
            ResourceTypeMapping resourceTypeMapping = new ResourceTypeMapping(
                clrType, 
                ResourceTypeKind.EntityType, 
                baseResourceType as ResourceTypeMapping, 
                this.namespaceName, 
                clrType.Name, 
                clrType.IsAbstract);
            this.RegisterResourceTypeMapping(resourceTypeMapping);
            return resourceTypeMapping;
        }

        /// <summary>Creates a resource type mapping for the specified CLR type which is either a primitive or complex type.</summary>
        /// <param name="clrType">The CLR instance client type.</param>
        /// <returns>The newly creates resource type.</returns>
        private ResourceType CreatePrimitiveOrComplexResourceTypeMapping(Type clrType)
        {
            ResourceType result = null;
            if (!this.resourceTypes.TryGetValue(clrType, out result))
            {
                result = ResourceType.GetPrimitiveResourceType(clrType);
                if (result == null)
                {
                    result = this.CreateComplexResourceTypeMapping(clrType);
                }
            }

            return result;
        }

        /// <summary>Creates a resource type mapping for the specified CLR type which is a complex type.</summary>
        /// <param name="clrType">The CLR instance client type.</param>
        /// <returns>The newly creates resource type.</returns>
        private ResourceTypeMapping CreateComplexResourceTypeMapping(Type clrType)
        {
            ResourceType baseResourceType;
            this.resourceTypes.TryGetValue(clrType.BaseType, out baseResourceType);
            ResourceTypeMapping resourceTypeMapping = new ResourceTypeMapping(
                clrType, 
                ResourceTypeKind.ComplexType, 
                baseResourceType as ResourceTypeMapping, 
                this.namespaceName, 
                clrType.Name, 
                clrType.IsAbstract);
            this.RegisterResourceTypeMapping(resourceTypeMapping);
            return resourceTypeMapping;
        }

        /// <summary>Registers newly create resource type mapping in the context and for processing.</summary>
        /// <param name="resourceTypeMapping">The resource type mapping to register.</param>
        private void RegisterResourceTypeMapping(ResourceTypeMapping resourceTypeMapping)
        {
            this.resourceTypes.Add(resourceTypeMapping.InstanceType, resourceTypeMapping);
            this.resourceTypeMappingsToProcess.Enqueue(resourceTypeMapping);
        }
    }
}
